// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'package:charts_grasp/src/chart/axis/axis.dart';
import 'package:charts_grasp/src/chart/axis/multi_level_labels.dart';
import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/cartesian_state_properties.dart';
import 'package:charts_grasp/src/chart/utils/enum.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/event_args.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' show DateFormat;

@immutable
class DateTimeCategoryAxis extends ChartAxis {
  DateTimeCategoryAxis({
    super.name,
    super.isVisible,
    super.title,
    super.axisLine,
    super.rangePadding,
    super.edgeLabelPlacement,
    super.labelPosition,
    super.tickPosition,
    super.labelRotation,
    super.labelIntersectAction,
    super.labelAlignment,
    super.isInversed,
    super.opposedPosition,
    super.minorTicksPerInterval,
    super.maximumLabels,
    super.majorTickLines,
    super.minorTickLines,
    super.majorGridLines,
    super.minorGridLines,
    super.labelStyle,
    super.plotOffset,
    super.zoomFactor,
    super.zoomPosition,
    super.interactiveTooltip,
    this.minimum,
    this.maximum,
    super.interval,
    this.visibleMinimum,
    this.visibleMaximum,
    super.crossesAt,
    super.associatedAxisName,
    super.placeLabelsNearAxisLine,
    super.plotBands,
    super.desiredIntervals,
    super.rangeController,
    super.maximumLabelWidth,
    super.labelsExtent,
    this.labelPlacement = LabelPlacement.betweenTicks,
    this.dateFormat,
    this.intervalType = DateTimeIntervalType.auto,
    this.autoScrollingDeltaType = DateTimeIntervalType.auto,
    super.autoScrollingDelta,
    super.borderWidth,
    super.borderColor,
    super.axisBorderType,
    super.multiLevelLabelStyle,
    super.multiLevelLabelFormatter,
    List<DateTimeCategoricalMultiLevelLabel>? super.multiLevelLabels,
    super.autoScrollingMode,
    super.axisLabelFormatter,
  });

  final DateFormat? dateFormat;

  final LabelPlacement labelPlacement;

  final DateTimeIntervalType intervalType;

  final DateTime? minimum;

  final DateTime? maximum;

  final DateTime? visibleMinimum;

  final DateTime? visibleMaximum;

  final DateTimeIntervalType autoScrollingDeltaType;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is DateTimeCategoryAxis &&
        other.name == name &&
        other.isVisible == isVisible &&
        other.title == title &&
        other.axisLine == axisLine &&
        other.rangePadding == rangePadding &&
        other.labelPlacement == labelPlacement &&
        other.edgeLabelPlacement == edgeLabelPlacement &&
        other.labelPosition == labelPosition &&
        other.tickPosition == tickPosition &&
        other.labelRotation == labelRotation &&
        other.labelIntersectAction == labelIntersectAction &&
        other.labelAlignment == labelAlignment &&
        other.isInversed == isInversed &&
        other.opposedPosition == opposedPosition &&
        other.minorTicksPerInterval == minorTicksPerInterval &&
        other.maximumLabels == maximumLabels &&
        other.majorTickLines == majorTickLines &&
        other.minorTickLines == minorTickLines &&
        other.majorGridLines == majorGridLines &&
        other.minorGridLines == minorGridLines &&
        other.labelStyle == labelStyle &&
        other.plotOffset == plotOffset &&
        other.zoomFactor == zoomFactor &&
        other.zoomPosition == zoomPosition &&
        other.interactiveTooltip == interactiveTooltip &&
        other.minimum == minimum &&
        other.maximum == maximum &&
        other.interval == interval &&
        other.visibleMinimum == visibleMinimum &&
        other.visibleMaximum == visibleMaximum &&
        other.crossesAt == crossesAt &&
        other.associatedAxisName == associatedAxisName &&
        other.placeLabelsNearAxisLine == placeLabelsNearAxisLine &&
        other.plotBands == plotBands &&
        other.desiredIntervals == desiredIntervals &&
        other.rangeController == rangeController &&
        other.maximumLabelWidth == maximumLabelWidth &&
        other.labelsExtent == labelsExtent &&
        other.autoScrollingDelta == autoScrollingDelta &&
        other.autoScrollingMode == autoScrollingMode &&
        other.intervalType == intervalType &&
        other.axisBorderType == axisBorderType &&
        other.borderColor == borderColor &&
        other.borderWidth == borderWidth &&
        other.multiLevelLabelStyle == multiLevelLabelStyle &&
        other.multiLevelLabels == multiLevelLabels &&
        other.multiLevelLabelFormatter == multiLevelLabelFormatter &&
        other.dateFormat == dateFormat &&
        other.axisLabelFormatter == axisLabelFormatter;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[
      name,
      isVisible,
      title,
      axisLine,
      rangePadding,
      labelPlacement,
      edgeLabelPlacement,
      labelPosition,
      tickPosition,
      labelRotation,
      labelIntersectAction,
      labelAlignment,
      isInversed,
      opposedPosition,
      minorTicksPerInterval,
      maximumLabels,
      majorTickLines,
      minorTickLines,
      majorGridLines,
      minorGridLines,
      labelStyle,
      plotOffset,
      zoomFactor,
      zoomPosition,
      interactiveTooltip,
      minimum,
      maximum,
      interval,
      visibleMinimum,
      visibleMaximum,
      crossesAt,
      associatedAxisName,
      placeLabelsNearAxisLine,
      plotBands,
      desiredIntervals,
      rangeController,
      maximumLabelWidth,
      labelsExtent,
      autoScrollingDelta,
      autoScrollingMode,
      intervalType,
      axisBorderType,
      borderColor,
      borderWidth,
      multiLevelLabelStyle,
      multiLevelLabels,
      multiLevelLabelFormatter,
      dateFormat,
      axisLabelFormatter,
    ];
    return Object.hashAll(values);
  }
}

class DateTimeCategoryAxisRenderer extends ChartAxisRenderer {
  DateTimeCategoryAxisRenderer(DateTimeCategoryAxis dateTimeCategoryAxis, CartesianStateProperties stateProperties) {
    _axisDetails = DateTimeCategoryAxisDetails(dateTimeCategoryAxis, stateProperties, this);
    AxisHelper.setAxisRendererDetails(this, _axisDetails);
  }

  late DateTimeCategoryAxisDetails _axisDetails;

  @override
  void applyRangePadding(VisibleRange range, num? interval) {
    ActualRangeChangedArgs rangeChangedArgs;
    if (_axisDetails.dateTimeCategoryAxis.labelPlacement == LabelPlacement.betweenTicks) {
      range.minimum -= 0.5;
      range.maximum += 0.5;
      range.delta = range.maximum - range.minimum;
    }

    if (_axisDetails.dateTimeCategoryAxis.isVisible &&
        !(_axisDetails.dateTimeCategoryAxis.minimum != null && _axisDetails.dateTimeCategoryAxis.maximum != null)) {
      _axisDetails.applyRangePaddings(this, _axisDetails.stateProperties, range, interval!);
    }

    calculateVisibleRange(Size(_axisDetails.rect.width, _axisDetails.rect.height));

    if ((_axisDetails.dateTimeCategoryAxis.visibleMinimum != null ||
            _axisDetails.dateTimeCategoryAxis.visibleMaximum != null) &&
        (_axisDetails.dateTimeCategoryAxis.visibleMinimum != _axisDetails.dateTimeCategoryAxis.visibleMaximum) &&
        (!_axisDetails.stateProperties.isRedrawByZoomPan)) {
      _axisDetails.stateProperties.isRedrawByZoomPan = false;
      _axisDetails.visibleRange!.minimum = _axisDetails.visibleMinimum != null
          ? _axisDetails.getEffectiveRange(
              DateTime.fromMillisecondsSinceEpoch(_axisDetails.visibleMinimum!.toInt()), true,)
          : _axisDetails.getEffectiveRange(_axisDetails.dateTimeCategoryAxis.visibleMinimum, true) ??
              _axisDetails.actualRange!.minimum;
      _axisDetails.visibleRange!.maximum = _axisDetails.visibleMaximum != null
          ? _axisDetails.getEffectiveRange(
              DateTime.fromMillisecondsSinceEpoch(_axisDetails.visibleMaximum!.toInt()), false,)
          : _axisDetails.getEffectiveRange(_axisDetails.dateTimeCategoryAxis.visibleMaximum, false) ??
              _axisDetails.actualRange!.maximum;
      if (_axisDetails.dateTimeCategoryAxis.labelPlacement == LabelPlacement.betweenTicks) {
        _axisDetails.visibleRange!.minimum = _axisDetails.visibleMinimum != null
            ? _axisDetails.getEffectiveRange(
                    DateTime.fromMillisecondsSinceEpoch(_axisDetails.visibleMinimum!.toInt()), true,)! -
                0.5
            : (_axisDetails.dateTimeCategoryAxis.visibleMinimum != null
                ? _axisDetails.getEffectiveRange(_axisDetails.dateTimeCategoryAxis.visibleMinimum, true)! - 0.5
                : _axisDetails.visibleRange!.minimum);
        _axisDetails.visibleRange!.maximum = _axisDetails.visibleMaximum != null
            ? _axisDetails.getEffectiveRange(
                    DateTime.fromMillisecondsSinceEpoch(_axisDetails.visibleMaximum!.toInt()), false,)! +
                0.5
            : (_axisDetails.dateTimeCategoryAxis.visibleMaximum != null
                ? _axisDetails.getEffectiveRange(_axisDetails.dateTimeCategoryAxis.visibleMaximum, false)! + 0.5
                : _axisDetails.visibleRange!.maximum);
      }
      _axisDetails.visibleRange!.delta = _axisDetails.visibleRange!.maximum - _axisDetails.visibleRange!.minimum;
      _axisDetails.visibleRange!.interval = interval == null
          ? calculateInterval(_axisDetails.visibleRange!, _axisDetails.axisSize)
          : _axisDetails.visibleRange!.interval;
      _axisDetails.zoomFactor = _axisDetails.visibleRange!.delta / (range.delta);
      _axisDetails.zoomPosition =
          (_axisDetails.visibleRange!.minimum - _axisDetails.actualRange!.minimum) / range.delta;
    }
    if (_axisDetails.chart.onActualRangeChanged != null) {
      rangeChangedArgs = ActualRangeChangedArgs(_axisDetails.name, _axisDetails.dateTimeCategoryAxis, range.minimum,
          range.maximum, range.interval, _axisDetails.orientation,);
      rangeChangedArgs.visibleMin = _axisDetails.visibleRange!.minimum;
      rangeChangedArgs.visibleMax = _axisDetails.visibleRange!.maximum;
      rangeChangedArgs.visibleInterval = _axisDetails.visibleRange!.interval;
      _axisDetails.chart.onActualRangeChanged!(rangeChangedArgs);
      _axisDetails.visibleRange!.minimum = rangeChangedArgs.visibleMin is DateTime
          ? _axisDetails.getEffectiveRange(rangeChangedArgs.visibleMin, true)
          : rangeChangedArgs.visibleMin;
      _axisDetails.visibleRange!.maximum = rangeChangedArgs.visibleMax is DateTime
          ? _axisDetails.getEffectiveRange(rangeChangedArgs.visibleMax, false)
          : rangeChangedArgs.visibleMax;
      _axisDetails.visibleRange!.delta = _axisDetails.visibleRange!.maximum - _axisDetails.visibleRange!.minimum;
      _axisDetails.visibleRange!.interval = rangeChangedArgs.visibleInterval;
      _axisDetails.zoomFactor = _axisDetails.visibleRange!.delta / (range.delta);
      _axisDetails.zoomPosition =
          (_axisDetails.visibleRange!.minimum - _axisDetails.actualRange!.minimum) / range.delta;
    }
  }

  @override
  void calculateVisibleRange(Size availableSize) {
    calculateDateTimeVisibleRange(availableSize, this);
  }

  @override
  void generateVisibleLabels() {
    _axisDetails.visibleLabels = <AxisLabel>[];
    num tempInterval = _axisDetails.visibleRange!.minimum.ceil();
    num interval = _axisDetails.visibleRange!.interval;
    interval = interval.toString().split('.').length >= 2
        ? interval.toString().split('.')[1].length == 1 && interval.toString().split('.')[1] == '0'
            ? interval.floor()
            : interval.ceil()
        : interval;
    int position;
    num prevInterval;
    String labelText;
    final List<AxisLabel> label = _axisDetails.visibleLabels;
    prevInterval =
        (label.isNotEmpty) ? _axisDetails.visibleLabels[_axisDetails.visibleLabels.length - 1].value : tempInterval;
    _axisDetails.dateTimeFormat = _axisDetails.dateTimeCategoryAxis.dateFormat ??
        getDateTimeLabelFormat(this, tempInterval.toInt(), prevInterval.toInt());
    for (; tempInterval <= _axisDetails.visibleRange!.maximum; tempInterval += interval) {
      if (withInRange(tempInterval, _axisDetails)) {
        position = tempInterval.round();
        if (position <= -1 || (_axisDetails.labels.isNotEmpty && position >= _axisDetails.labels.length)) {
          continue;
        } else if (_axisDetails.labels.isNotEmpty) {
          labelText = _axisDetails.getFormattedLabel(_axisDetails.labels[position], _axisDetails.dateFormat);
          _axisDetails.labels[position] = labelText;
        } else {
          continue;
        }
        _axisDetails.triggerLabelRenderEvent(
            labelText, tempInterval, _axisDetails.actualIntervalType, _axisDetails.dateTimeFormat!.pattern,);

        if (interval == 0) {
          break;
        }
      }
    }

    _axisDetails.calculateMaximumLabelSize(this, _axisDetails.stateProperties);
    if (_axisDetails.dateTimeCategoryAxis.multiLevelLabels != null &&
        _axisDetails.dateTimeCategoryAxis.multiLevelLabels!.isNotEmpty) {
      generateMultiLevelLabels(_axisDetails);
      calculateMultiLevelLabelBounds(_axisDetails);
    }
  }

  @override
  num calculateInterval(VisibleRange range, Size availableSize) {
    return _axisDetails.calculateNumericNiceInterval(this, range.delta, availableSize);
  }
}

class DateTimeCategoryAxisDetails extends ChartAxisRendererDetails {
  DateTimeCategoryAxisDetails(
      this.dateTimeCategoryAxis, CartesianStateProperties stateProperties, ChartAxisRenderer axisRenderer,)
      : super(dateTimeCategoryAxis, stateProperties, axisRenderer) {
    labels = <String>[];
  }

  late List<String> labels;

  late Rect rect;

  final DateTimeCategoryAxis dateTimeCategoryAxis;

  late DateTimeIntervalType actualIntervalType;

  DateFormat? dateTimeFormat;

  DateFormat get dateFormat => dateTimeFormat ?? getDateTimeLabelFormat(axisRenderer);

  void findAxisMinMaxValues(
      SeriesRendererDetails seriesRendererDetails, CartesianChartPoint<dynamic> point, int pointIndex, int dataLength,
      [bool? isXVisibleRange, bool? isYVisibleRange,]) {
    final bool anchorRangeToVisiblePoints = seriesRendererDetails.yAxisDetails!.axis.anchorRangeToVisiblePoints;
    final String seriesType = seriesRendererDetails.seriesType;

    if (!labels.contains('${point.x.microsecondsSinceEpoch}')) {
      labels.add('${point.x.microsecondsSinceEpoch}');
    }
    point.xValue = labels.indexOf('${point.x.microsecondsSinceEpoch}');
    point.yValue = point.y;
    if (isYVisibleRange!) {
      seriesRendererDetails.minimumX ??= point.xValue;
      seriesRendererDetails.maximumX ??= point.xValue;
    }
    if ((isXVisibleRange! || !anchorRangeToVisiblePoints) &&
        !seriesType.contains('range') &&
        !seriesType.contains('hilo') &&
        !seriesType.contains('candle') &&
        seriesType != 'boxandwhisker' &&
        seriesType != 'waterfall') {
      seriesRendererDetails.minimumY ??= point.yValue;
      seriesRendererDetails.maximumY ??= point.yValue;
    }
    setCategoryMinMaxValues(
        axisRenderer, isXVisibleRange, isYVisibleRange, point, pointIndex, dataLength, seriesRendererDetails,);
  }

  void _controlListener() {
    stateProperties.canSetRangeController = false;
    if (axis.rangeController != null && !stateProperties.rangeChangedByChart) {
      updateRangeControllerValues(this);
      stateProperties.rangeChangeBySlider = true;
      stateProperties.redrawByRangeChange();
    }
  }

  void calculateRangeAndInterval(CartesianStateProperties stateProperties, [String? type]) {
    chart = stateProperties.chart;
    if (axis.rangeController != null) {
      stateProperties.rangeChangeBySlider = true;
      axis.rangeController!.addListener(_controlListener);
    }
    final Rect containerRect = stateProperties.renderingDetails.chartContainerRect;
    rect = Rect.fromLTWH(containerRect.left, containerRect.top, containerRect.width, containerRect.height);
    axisSize = Size(rect.width, rect.height);
    axisRenderer.calculateRange(axisRenderer);
    _calculateActualRange();
    if (actualRange != null) {
      axisRenderer.applyRangePadding(actualRange!, actualRange!.interval);
      if (type == null && type != 'AxisCross' && dateTimeCategoryAxis.isVisible) {
        axisRenderer.generateVisibleLabels();
      }
    }
  }

  void _calculateActualRange() {
    min ??= 0;
    max ??= 5;
    actualRange = VisibleRange(
        dateTimeCategoryAxis.minimum != null ? getEffectiveRange(dateTimeCategoryAxis.minimum, true) : min,
        dateTimeCategoryAxis.maximum != null ? getEffectiveRange(dateTimeCategoryAxis.maximum, false) : max,);

    if ((actualRange!.minimum == actualRange!.maximum) &&
        (dateTimeCategoryAxis.labelPlacement == LabelPlacement.onTicks)) {
      actualRange!.maximum += 1;
    }
    if (labels.isNotEmpty) {
      final List<DateTime> startAndEnd = _getStartAndEndDate(labels);
      calculateDateTimeNiceInterval(axisRenderer, axisSize, actualRange!, startAndEnd[0], startAndEnd[1]);
    } else {
      actualIntervalType = DateTimeIntervalType.days;
    }
    actualRange!.delta = actualRange!.maximum - actualRange!.minimum;
    actualRange!.interval =
        dateTimeCategoryAxis.interval ?? axisRenderer.calculateInterval(actualRange!, Size(rect.width, rect.height));
  }

  String getFormattedLabel(String label, DateFormat dateFormat) {
    return dateFormat.format(DateTime.fromMicrosecondsSinceEpoch(int.parse(label)));
  }

  List<DateTime> _getStartAndEndDate(List<String> labels) {
    List<String> values = <String>[];
    for (final String label in labels) {
      if (label.contains(RegExp(r'^-?[0-9]+$')) &&
          (int.parse(label)) > 2592000000) {
        values.add(label);
      } else {
        values.add(dateFormat.parse(label).microsecondsSinceEpoch.toString());
      }
    }
    values = <String>[...values]..sort((first, second) {
        return int.parse(first) < int.parse(second) ? -1 : 1;
      });
    return <DateTime>[
      DateTime.fromMicrosecondsSinceEpoch(int.parse(values.first)),
      DateTime.fromMicrosecondsSinceEpoch(int.parse(values.last)),
    ];
  }

  num? getEffectiveRange(DateTime? rangeDate, bool needMin) {
    num index = 0;
    if (rangeDate == null) {
      return null;
    }
    for (final String label in labels) {
      final int value = int.parse(label);
      if (needMin) {
        if (value > rangeDate.microsecondsSinceEpoch) {
          if (!(labels.first == label)) {
            index++;
          }
          break;
        } else if (value < rangeDate.microsecondsSinceEpoch) {
          index = labels.indexOf(label);
        } else {
          index = labels.indexOf(label);
          break;
        }
      } else {
        if (value <= rangeDate.microsecondsSinceEpoch) {
          index = labels.indexOf(label);
        }
        if (value >= rangeDate.microsecondsSinceEpoch) {
          break;
        }
      }
    }
    return index;
  }
}
